//
// App delegate
//

#import "app_delegate.h"
#import "Manta.h"
#import "MantaExceptions.h"
#import "RtMidi.h"
#import <sys/time.h>


const double NOTE_OFF          = 0;
const double NOTE_MIN_DURATION = 0.0; // Disable min duration

static int base_offset[12] = {-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8};

static int layout_notes[6][48] =
{
    // Chromatic
    {
        36, 37, 38, 39, 40, 41, 42, 43,
        44, 45, 46, 47, 48, 49, 50, 51,
        52, 53, 54, 55, 56, 57, 58, 59,
        60, 61, 62, 63, 64, 65, 66, 67,
        68, 69, 70, 71, 72, 73, 74, 75,
        76, 77, 78, 79, 80, 81, 82, 83
    },

    // Major
    {
        24, 26, 28, 29, 31, 33, 35, 36,
        36, 38, 40, 41, 43, 45, 47, 48,
        48, 50, 52, 53, 55, 57, 59, 60,
        60, 62, 64, 65, 67, 69, 71, 72,
        72, 74, 76, 77, 79, 81, 83, 84,
        84, 86, 88, 89, 91, 93, 95, 96,
    },

    // Minor
    {
        24, 26, 27, 29, 31, 32, 34, 36,
        36, 38, 39, 41, 43, 44, 46, 48,
        48, 50, 51, 53, 55, 56, 58, 60,
        60, 62, 63, 65, 67, 68, 70, 72,
        72, 74, 75, 77, 79, 80, 82, 84,
        84, 86, 87, 89, 91, 92, 94, 96,
    },

    // Honeycomb
    {
        32, 35, 38, 53, 56, 59, 62, 77,
        40, 43, 46, 61, 64, 67, 70, 85,
        24, 36, 39, 54, 57, 60, 75, 78,
        41, 44, 47, 62, 65, 68, 71, 86,
        34, 37, 52, 55, 58, 73, 76, 79,
        42, 45, 48, 63, 66, 69, 72, 87
    },

    // Harmonic
    {
        24, 28, 32, 36, 40, 44, 48, 52,
        31, 35, 39, 43, 47, 51, 55, 59,
        34, 38, 42, 46, 50, 54, 58, 62,
        41, 45, 49, 53, 57, 61, 65, 69,
        44, 69, 70, 71, 72, 73, 74, 75,
        51, 55, 59, 63, 67, 71, 75, 79
    },

    // Wicki-Hayden
    {
        36, 38, 40, 42, 44, 46, 48, 50,
        43, 45, 47, 49, 51, 53, 55, 57,
        48, 50, 52, 54, 56, 58, 60, 62,
        55, 57, 59, 61, 63, 65, 67, 69,
        60, 62, 64, 66, 68, 70, 72, 74,
        67, 69, 71, 73, 75, 77, 79, 81
    },
};

class MantaObject: public Manta
{
public:
    MantaObject(AppDelegate* app)
    : _app(app)
    {
    }

    ~MantaObject()
    {
    }

    void PadEvent(int row, int column, int idx, int value)
    {
        [_app onPadEvent: idx withValue: value];
    }
    
    void SliderEvent(int idx, int value)
    {
        [_app onSliderEvent: idx withValue: value];
    }

    void ButtonEvent(int idx, int value)
    {
    }
    
    void PadVelocityEvent(int row, int column, int idx, int velocity)
    {
        [_app onPadVelocityEvent: idx withVelocity: velocity];
    }
    
    void ButtonVelocityEvent(int idx, int velocity)
    {
        [_app onButtonEvent: idx withValue: velocity];
    }
    
    void FrameEvent(uint8_t *frame)
    {
    }
    
    void DebugPrint(const char *fmt, ...)
    {
        va_list args;
        va_start(args, fmt);

        char buffer[1024];
        vsnprintf(buffer, 1024, fmt, args);
        va_end(args);

        NSLog([NSString stringWithCString: buffer encoding: NSASCIIStringEncoding]);
    }


private:
    AppDelegate* _app;
};


@implementation AppDelegate


- (id) init
{
    self = [super init];
    if (self)
    {
        _state = STATE_DISCONNECTED;
        _midi  = new RtMidiOut();
        _midi->openVirtualPort("MacManta");

        _slider_val[0] = -1;
        _slider_val[1] = -1;

        _button_val[0] = -1;
        _button_val[1] = -1;
        _button_val[2] = -1;
        _button_val[3] = -1;

        for (int ii = 0; ii < 48; ii++)
        {
            _pad_time[ii]  = NOTE_OFF;
            _pad_val[ii]   = -1;

            _index_to_pitch[ii] = (unsigned char)ii;
        }

        _pad_cc  = -1;
        _pad_max = -1;

        _pending = new std::queue<int>();

        [NSThread detachNewThreadSelector: @selector(mantaThread) toTarget: self withObject: nil];
    }

    return self;
}


-(void) applicationDidFinishLaunching: (NSNotification*) aNotification
{
}


-(void) applicationWillTerminate: (NSNotification*) aNotification
{
}


-(void) awakeFromNib
{
    // Load status bar item
    _statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength: NSVariableStatusItemLength] retain];
    [_statusItem setImage: [NSImage imageNamed: @"m.png"]];


    // Create menu
    NSMenu* menu = [[[NSMenu alloc] initWithTitle: @"MacManta"] autorelease];

    NSMenuItem* title = [[[NSMenuItem alloc] initWithTitle: @"MacManta v0.4" action: nil keyEquivalent: @""] autorelease];
    [title setEnabled: NO];
    [menu addItem: title];

    [menu addItem: [NSMenuItem separatorItem]];


    _connectItem = [[NSMenuItem alloc] initWithTitle: @"Connect..." action: @selector(onConnect) keyEquivalent: @""];
    [_connectItem setHidden: NO];
    [menu addItem: _connectItem];


    _mantaItem = [[NSMenuItem alloc] initWithTitle: @"manta" action: nil keyEquivalent: @""];
    [_mantaItem setEnabled: NO];
    [_mantaItem setHidden: YES];
    [menu addItem: _mantaItem];


    NSMenuItem* layout = [[[NSMenuItem alloc] initWithTitle: @"Layout" action: nil keyEquivalent: @""] autorelease];
    NSMenu* submenu = [[[NSMenu alloc] initWithTitle: @"Layout"] autorelease];

    NSString* layouts[] =
    {
        @"Chromatic",
        @"Major",
        @"Minor",
        @"Honeycomb",
        @"Harmonic",
        @"Wicki-Hayden"
    };

    for (int ii = 0; ii < 6; ii++)
    {
        _layoutItem[ii] = [[NSMenuItem alloc] initWithTitle: layouts[ii] action: @selector(onLayout:) keyEquivalent: @""];
        [_layoutItem[ii] setTag: ii];
        [submenu addItem: _layoutItem[ii]];
    }

    [layout setSubmenu: submenu];
    [menu addItem: layout];


    NSMenuItem* base = [[[NSMenuItem alloc] initWithTitle: @"Base Note" action: nil keyEquivalent: @""] autorelease];
    NSMenu* basemenu = [[[NSMenu alloc] initWithTitle: @"Base Note"] autorelease];

    NSString* bases[] =
    {
        @"A",
        @"A♯/B♭",
        @"B",
        @"C",
        @"C♯/D♭",
        @"D",
        @"D♯/E♭",
        @"E",
        @"F",
        @"F♯/G♭",
        @"G",
        @"G♯/A♭",
    };

    for (int ii = 0; ii < 12; ii++)
    {
        _baseItem[ii] = [[NSMenuItem alloc] initWithTitle: bases[ii] action: @selector(onBase:) keyEquivalent: @""];
        [_baseItem[ii] setTag: ii];
        [basemenu addItem: _baseItem[ii]];
    }

    [base setSubmenu: basemenu];
    [menu addItem: base];



    NSMenuItem* pressure = [[[NSMenuItem alloc] initWithTitle: @"Pressure" action: nil keyEquivalent: @""] autorelease];
    NSMenu* pressmenu = [[[NSMenu alloc] initWithTitle: @"Pressure"] autorelease];

    _pressOffItem   = [[NSMenuItem alloc] initWithTitle: @"Off" action: @selector(onPressureOff) keyEquivalent: @""];
    _pressAfterItem = [[NSMenuItem alloc] initWithTitle: @"Polyphonic AT" action: @selector(onPressureAftertouch) keyEquivalent: @""];
    _pressCCItem    = [[NSMenuItem alloc] initWithTitle: @"Channel AT" action: @selector(onPressureCC) keyEquivalent: @""];

    [pressmenu addItem: _pressOffItem];
    [pressmenu addItem: _pressAfterItem];
    [pressmenu addItem: _pressCCItem];

    [pressure setSubmenu: pressmenu];
    [menu addItem: pressure];


    [menu addItem: [NSMenuItem separatorItem]];

    NSMenuItem* quit = [[[NSMenuItem alloc] initWithTitle: @"Quit" action: @selector(onQuit) keyEquivalent: @""] autorelease];
    [quit setTarget: self];
    [menu addItem: quit];


    // Add to status bar
    [_statusItem setMenu: menu];
    [_statusItem setHighlightMode: YES];


    // Attempt to connect
    [self onConnect];
    [self setLayout: 3 baseNote: 3];
    [self setPressure: PRESSURE_CHANNEL];
}


-(double) now
{
    struct timeval t;
    gettimeofday(&t, NULL);

    return (double)(t.tv_sec) + (double)(t.tv_usec) * 1.0e-6;
}


-(void) mantaThread
{
    MantaObject* manta = nil;

    // Make this thread higher priority than normal...
    [NSThread setThreadPriority: [NSThread threadPriority] + 0.2];

    while (_state != STATE_QUIT)
    {
        if (_state == STATE_CONNECTING)
        {
            manta = new MantaObject(self);

            try
            {
                manta->Connect();

                NSNumber* serial = [NSNumber numberWithInt: manta->GetSerialNumber()];

                [self setStateFrom: STATE_CONNECTING to: STATE_CONNECTED];
                [self performSelectorOnMainThread: @selector(onConnected:) withObject: serial waitUntilDone: NO];
            }
            catch (...)
            {
                NSLog(@"Failed to connect...");

                [self setStateFrom: STATE_CONNECTING to: STATE_DISCONNECTED];
            }
        }

        if (_state == STATE_CONNECTED)
        {
            try
            {
                manta->HandleEvents();
            }
            catch (...)
            {
                NSLog(@"Disconnected...");

                // Send all notes off...
                std::vector<unsigned char> msg;

                msg.push_back(0xb0);
                msg.push_back(0x7b);
                msg.push_back(0x00);

                _midi->sendMessage(&msg);

                [self setStateFrom: STATE_CONNECTED to: STATE_DISCONNECTED];
                [self performSelectorOnMainThread: @selector(onDisconnected) withObject: nil waitUntilDone: NO];
            }
        }

        if (_state == STATE_DISCONNECTED)
        {
            if (manta)
            {
                delete manta;
                manta = nil;
            }
        }

        // Send any pending note off
        if (!_pending->empty())
        {
            double now = [self now];

            while (!_pending->empty())
            {
                int idx = _pending->front();
                if (_pad_time[idx] == NOTE_OFF)
                {
                    _pending->pop();
                }
                else if (now - _pad_time[idx] > NOTE_MIN_DURATION)
                {
                    [self sendNoteOff: idx];
                    _pending->pop();
                }
                else
                {
                    break;
                }
            }
        }
            
        [NSThread sleepForTimeInterval: 0.002];
    }
}


-(void) setStateFrom: (MantaState) from to: (MantaState) to
{
    if (_state == from)
        _state = to;
}


-(void) setLayout: (int) idx baseNote: (int) base
{
    for (int ii = 0; ii < 6; ii++)
    {
        if (ii == idx)
            [_layoutItem[ii] setState: NSOnState];
        else
            [_layoutItem[ii] setState: NSOffState];
    }

    for (int ii = 0; ii < 12; ii++)
    {
        if (ii == base)
            [_baseItem[ii] setState: NSOnState];
        else
            [_baseItem[ii] setState: NSOffState];
    }

    for (int ii = 0; ii < 48; ii++)
        _index_to_pitch[ii] = layout_notes[idx][ii] + base_offset[base];

    _layout_index = idx;
    _base_index   = base;
}


-(void) setPressure: (PressureState) state
{
    if (state == PRESSURE_OFF)
    {
        [_pressOffItem setState: NSOnState];
        [_pressAfterItem setState: NSOffState];
        [_pressCCItem setState: NSOffState];
    }
    else if (state == PRESSURE_POLYPHONIC)
    {
        [_pressOffItem setState: NSOffState];
        [_pressAfterItem setState: NSOnState];
        [_pressCCItem setState: NSOffState];
    }
    else if (state == PRESSURE_CHANNEL)
    {
        [_pressOffItem setState: NSOffState];
        [_pressAfterItem setState: NSOffState];
        [_pressCCItem setState: NSOnState];
    }

    _pressure = state;
}


-(void) onQuit
{
    [NSApp terminate: self];
}


-(void) onLayout: (NSMenuItem*) item
{
    [self setLayout: [item tag] baseNote: _base_index];
}


-(void) onBase: (NSMenuItem*) item
{
    [self setLayout: _layout_index baseNote: [item tag]];
}


-(void) onPressureOff
{
    [self setPressure: PRESSURE_OFF];
}


-(void) onPressureAftertouch
{
    [self setPressure: PRESSURE_POLYPHONIC];
}


-(void) onPressureCC
{
    [self setPressure: PRESSURE_CHANNEL];
}


-(void) onConnect
{
    [self setStateFrom: STATE_DISCONNECTED to: STATE_CONNECTING];
}


-(void) onConnected: (NSNumber*) serial_num
{
    [_mantaItem setTitle: [NSString stringWithFormat: @"Connected to Manta (%@)", serial_num]];
    [_mantaItem setHidden: NO];
    [_connectItem setHidden: YES];
}


-(void) onDisconnected
{
    [_mantaItem setHidden: YES];
    [_connectItem setHidden: NO];
}


-(void) findMaxPressure
{
    _pad_cc  = 0;
    _pad_max = -1;

    for (int ii = 0; ii < 48; ii++)
    {
        if (_pad_val[ii] > _pad_cc)
        {
            _pad_cc  = _pad_val[ii];
            _pad_max = ii;
        }
    }
}


-(void) sendMaxPressure
{
    std::vector<unsigned char> msg;

    msg.push_back(0xd0);
    msg.push_back(_pad_cc);
    _midi->sendMessage(&msg);
}


-(void) sendNoteOff: (int) idx
{
    if (_pad_time[idx] != NOTE_OFF)
    {
        std::vector<unsigned char> msg;

        msg.push_back(0x80);
        msg.push_back(_pad_note[idx]);
        msg.push_back(0x00);

        _midi->sendMessage(&msg);

        _pad_time[idx] = NOTE_OFF;
        _pad_val[idx]  = 0;

        // If that was the max CC note then figure out new max...
        if (idx == _pad_max && _pressure == PRESSURE_CHANNEL)
        {
            [self findMaxPressure];
            [self sendMaxPressure];
        }
    }
}


-(void) onPadEvent: (int) idx withValue: (int) value
{
    if (_pad_time[idx] != NOTE_OFF)
    {
        int val = value * 127 / 210;
        if (val != _pad_val[idx])
        {
            _pad_val[idx] = val;

            if (_pressure == PRESSURE_POLYPHONIC)
            {
                std::vector<unsigned char> msg;

                msg.push_back(0xa0);
                msg.push_back(_pad_note[idx]);
                msg.push_back(_pad_val[idx]);

                _midi->sendMessage(&msg);
            }
            else if (_pressure == PRESSURE_CHANNEL)
            {
                if (val < _pad_cc && idx == _pad_max)
                {
                    [self findMaxPressure];
                    [self sendMaxPressure];
                }
                else if (val > _pad_cc)
                {
                    _pad_max = idx;
                    _pad_cc  = val;

                    [self sendMaxPressure];
                }
            }
        }
    }
}


-(void) onPadVelocityEvent: (int) idx withVelocity: (int) velocity
{
    if (velocity > 0)
    {
        // Note on
        if (_pad_time[idx] == NOTE_OFF)
        {
            _pad_note[idx] = _index_to_pitch[idx];

            std::vector<unsigned char> msg;

            msg.push_back(0x90);
            msg.push_back(_pad_note[idx]);
            msg.push_back(velocity);

            _midi->sendMessage(&msg);

            _pad_time[idx] = [self now];
        }
    }
    else
    {
        // Note off
        if (_pad_time[idx] != NOTE_OFF)
        {
            double now = [self now];

            if (now - _pad_time[idx] < NOTE_MIN_DURATION)
            {
                // Too short a note - queue to send later
                _pending->push(idx);
            }
            else
            {
                [self sendNoteOff: idx];
            }
        }
    }
}


-(void) onSliderEvent: (int) idx withValue: (int) value
{
    if (value < 65535)
    {
        int cc = value / 32;
        if (_slider_val[idx] != cc)
        {
            std::vector<unsigned char> msg;

            msg.push_back(0xb0);
            msg.push_back(0x01 + idx);
            msg.push_back(cc);
            _midi->sendMessage(&msg);

            _slider_val[idx] = cc;
        }
    }
}


-(void) onButtonEvent: (int) idx withValue: (int) value
{
    int cc = (value > 0) ? 0x7f : 0x00;
    if (_button_val[idx] != cc)
    {
        std::vector<unsigned char> msg;

        msg.push_back(0xb0);
        msg.push_back(0x40 + idx);
        msg.push_back(cc);
        _midi->sendMessage(&msg);

        _button_val[idx] = cc;
    }
}


@end

